ユニットテスト改善ガイド
先日、日本Javaユーザグループ(JJUG)主催のJJUG CCC 2013 Fallで、「ユニットテスト改善ガイド」というタイトルで登壇してきました。自分の経験を元に、ユニットテストをチームや組織へ導入する時に起こりえる問題とその解決のヒントに関するセッションです。本エントリーではそのセッションの内容を再構成して公開します。
はじめに
近年のシステム開発では、ユニットテストや継続的インテグレーション(以下、CI)の導入は必要不可欠と考えられています。とはいえ、どんな組織(チーム)でも簡単に導入できているわけではありません。特に、大きな組織や古くからの慣習を残している組織では導入したくとも中々進まないと感じているところが多いのではないでしょうか?。
私は、これまでに多くの開発現場でユニットテストやCIの導入について推進してきました。成功したケースもあれば失敗したケースもあります。そして、失敗した要因は、ユニットテストにまつわる多くの勘違いが原因だと考えています。
先日、ユニットテストにまつわる10の勘違いというエントリーを書きました。このエントリーでは、間違った形であればユニットテストを実践しない方が良いというスタンスで、ユニットテストが残念な形で導入されるパターンを紹介しています。これだけでは、自分がユニットテストの否定派のようにも読めるかもしれません。どんな勘違いな方法でもユニットテストを書かないよりは書いた方がマシだと感じた方もいると思います。
そこで、先日のJJUG CCCにおいて「どうすればユニットテストの導入に失敗しないのか?」という視点で話すことにしました。本エントリーは、そのJJUG CCCで話した内容を参加されていない方にも伝わるようにまとめなおしたものです。セッションではもう少し具体的な話なども出していましたが、その辺は参加された方の特典であり、ブログに書く内容でもないということでご理解ください。
ユニットテスト改善ガイド
それでは、ユニットテストを導入していく中でありがちな問題や疑問に対して、問題の原因と解決のヒントを紹介します。各問題はそれぞれが独立しているわけではなく、同じ原因を多く含んでいます。したがって、原因や解決方法のヒントには重複が含まれます。あらかじめご了承ください。
また、本エントリーで紹介する内容は、筆者の経験などから導き出した解決方法のヒントでしかありません。適用する組織、プロジェクトにより最適な方法は異なります。
ユニットテストの書き方がわかりません
ユニットテストは誰にでもできる簡単な作業ではありません。テストする対象となるクラスやメソッドを設計し実装するだけのスキルがなければ、どのように振る舞うかを予想できません。また、潜在的な問題があったとしても気付くことはできないでしょう。さらに、テスト技法を知らなければ、どのようなテストデータを選択し、どの程度のテストを実行すれば十分かを判断することもできません。
ユニットテストを習得するには学ぶしかありません。自ら学習する機会を作ることがない人を相手にするならば、トレーニングを実施するしかありません。しかし、余分な作業を押しつけても、拒絶されるだけです。トレーニングはプロジェクトのコストに含めず、可能な限り業務時間でやることが必要となります。仕事に必要なスキルなのですから、業務時間に習得させるべきでしょう。
トレーニングを行ったとしても、十分なスキルを身につけるまでは多くの時間が必要です。効率的にトレーニングを行うのであれば、コーチングができる経験者を探してください。
自ら学習する機会を探す人であれば、たくさんの参考書があります。テスト技法であれば「はじめて学ぶソフトウェアのテスト技法」、プログラミングで役に立つのは「リファクタリング」がオススメです。Javaであれば「JUnit実践入門」もよろしくお願いします。たくさんのサンプルテストコードを収録しているので、それらを書き写していくだけでも力になると思います。
どこからユニットテストをはじめたら良いか解りません
どんなクラスやメソッドでも同じようにユニットテストできるわけではありません。テストが簡単なクラスもあれば難しいクラスもあります。テストが効果的なクラスもあれば、それほど効果的でないクラスもあります。全てをカバーしようとすれば時間的に困難となるだけでなく、難しいテストも相手にすることになります。
ユニットテストしやすいメソッドは、シンプルで、状態を持たず、期待する結果が不定とならないメソッドです。Javaでいえば、そのようなクラスの代表例はユーティリティクラスと呼ばれるクラスのメソッドです。そのような文字列の処理や数値の計算など、入力値に対して期待する結果となるかをテストするのは簡単です。
組織ではじめてユニットテストを導入するのであれば、このようなテストしやすいユーティリティクラスのテストからはじめると良いでしょう。例えば、次のコードに含まれる正規表現に自信が持てないケースを考えます。
// その他色々な処理 String code = "1234-1234-0000"; if (!code.matches("\\d{4}\\-\\d{4}\\-\\d{4}")) { // コードが正しくない場合の処理 } // その他色々な処理
正規表現によるマッチングのみをユーティリティメソッドに抽出すると、このようになります。
if (!CodeUtils.isValid(code)) { // コードが正しくない場合の処理 }
class CodeUtils { public static boolean isValid(String code) { return code.matches("\\d{4}\\-\\d{4}\\-\\d{4}"); } }
このCodeUtilsクラスのユニットテストは簡単です。マッチングするような入力値とマッチングしない入力値について、次のように検証すればよいでしょう。
class CodeUtilsTest { @Test public void isValidは正しいコードでtrue() { String code = "1234-1234-1234"; assertThat(CodeUtils.isValid(code), is(true)); } @Test public void isValidはハイフン無でfalse() { String code = "123412341234"; assertThat(CodeUtils.isValid(code), is(false)); } }
ユーティリティクラスのように簡単なクラスのテストからはじめ、慣れてきたならば他のクラスもテストしてください。経験を積み重ね、テストできるクラスの種類を増やしていきましょう。最初から全てをテストしようとしないでください。
対象のコードがテストしにくいです・・・
ユニットテストの効果は、テスト対象のメソッド(API)が期待通りに振る舞うかを保証するだけではありません。テストコードを書くプログラマが実際に利用することで、使いやすいか?違和感はないか?といったことを体験することも重要な効果です。そして、使いにくいと感じたならば積極的に改善します。
テストファーストについて学ぶことも効果的です。テスト対象が使いにくいと感じた時、書き終えたテスト対象を変更する事には少なからず抵抗があるでしょう。しかし、テスト対象を書く前に変更するには抵抗がありません。したがって、テストファーストを導入すれば、自然にテスト対象コードはテストしやすくなっていきます。ただし、何時でもテストファーストを実践しろとプロジェクトで強制するのではなく、ひとつの手段として扱ってください。
もし、ユニットテストが仕様書に定義された通りに動作する事を検証することとしたり、テスト対象となるメソッドを変更してはならないという制約があるならば、ユニットってストの効果は小さくなります。また、プログラマに多くのストレスを与えるでしょう。そのような環境では、はやく環境を改善するか、ストレスのない環境を求めて旅立つ方が賢明かもしれません。
テストしにくいコードは多くあります。しかし、それらのコードを使いにくいと感じたならば積極的に修正するチャンスと捉えてください。
継続的インテグレーション(CI)を導入したいです
CIの導入は簡単ですが、運用していくとなると簡単とは言えません。数値やルールが一人歩きしてしまい、プロジェクトが以前よりも進めにくくなってしまう場合もあります。テストが不十分であれば、CIを導入したとしても十分な効果が得られません。はじめはテスト文化を作っていくことに集中すべきです。
勿論、ユニットテストを進めていく上でもCIには様々な効果があります。なにより、継続的にリリースしていく中でリグレッション(デグレーション)の発生を早い段階で検知できです。リグレッションを完璧に防げるわけではありませんが、イージーミスで大きな不具合を混入させるといったリスクは激減するでしょう。ただし、それはユニットテストが充分に行われていることが前提なのです。
CIはユニットテストを組織に広めていく中で、大きな説得材料になります。それは自動化のプロセスが見えるようになるためです。テスト件数が増えていくのが可視化されると、プログラマのモチベーションも高まります。ユニットテストの実績が数値として可視化されるため、管理者層に説明しやすくなります。ただし、管理者層に説明する時は、それぞれの数字が何を意味しているかを正しく説明してください。説明する相手が都合の良いように解釈する傾向がある場合は避けた方が良いかもしれません。さらに、何時の間にか話が変わってしまうこともあります。しっかりとドキュメントやプレゼンテーション資料として残しておくことを強くオススメします。
なお、CIをはじめたい場合、自分のローカル環境にCI環境を構築し、練習をしておくことをオススメします。ローカル環境でJenkinsなどを動かし、定期的にビルドやテストを流してみましょう。仮想マシンなどにインストールし、Jenkinsサーバの運用についてノウハウを溜めるのもオススメします。GradleやMavenなどのビルドツールの使い方もあわせて習得するのも忘れないでください。そうすることで、組織に導入する敷居が低くなります。
テストコードのメンテナンスが大変です
テストコードを書いたならば、それで完了ではありません。プロダクトと同様に、リリースした後も継続的にメンテナンスしなければ、テストコードは技術的負債にすらなりえます。
特定のクラスをテストするとき、正常系と異常系のようにパラメータだけを変更して同じようなテストを実施します。したがって、テストコードは同じようなコードが多く含まれます。それらを放置していくと、テストの方法を変更するために多数のテストコードに変更をしなければならなくなります。
テストコードは定期的にリファクタリングし、重複を減らさなければなりません。JUnitであれば、MatcherやRuleを使いこなしてください。そして、それらのリファクタリングはプロダクションコードの作成、テストコードの作成と同時にやっていく必要があります。後から整理しようとしても手遅れです。ユニットテストを導入する場合は、テストコードのリファクタリング時間も見積もりに含めてください。
ユニットテストはプロジェクトを進めるプロセスの一部です。その効果は継続的なリリースを行っているほど効果的です。単発プロジェクトや1年以上もかかる大きなプロジェクトでは、テストコードのメンテナンスコストも考慮した上で、効果が十分かどうかを検討してから導入してください。
他のメンバーがユニットテストに興味がありません
あなた以外のメンバーがユニットテストに興味がない時、強制的に導入したところで上手くいくわけがありません。効果や目的を説明した上で、自然に導入していく必要があります。
ひとつの方法としては、デバッグツールとして広めていくことです。プロジェクトのメンバーがデバッグで困っている時、手助けする手段として、デバッガや標準出力を使わず、テストコードを書いてみましょう。ユニットテストを使って問題を解決し、そのテストコードが再利用可能であったりリグレッションを防ぐ効果などもあると解れば、きっと興味を持ってもらえるでしょう。標準出力やデバッカの代わりにユニットテストを書くことが選択肢に入るかもしれません。
また、プロジェクトの進め方を提案できる立場にいるならば、従来行っていたドキュメントベースの単体テストをユニットテストで置き換えることを提案してください。もし、それまでと同様に某表計算ソフトを使ってテストケースを抽出し、手動でチェックする単体テストを行いつつ、JUnitなどを利用したユニットテストを実践することになったならば、恨まれることはあっても喜ばれることはありません。
実は、ユニットテストに否定的なエンジニアはそれほど多くありません。日本人の性格的な問題もあります。例えば、6人チームで1人だけ新しい事を導入したいと考えている状況では、多くの人は「面倒だし、少数派は怖い」と思います。しかし、6人チームで2人が新しい事を導入したい状況では、2人ほどは「それならば…」と仲間に引き込める可能性が高くなります。どうしてもやりたくない人は、1人くらいなのです。したがって、はじめに興味を持ちそうな仲間を1-2名見つけ、先にユニットテストを学んで貰うのも良い作戦です。組織にユニットテストを導入するには外堀を埋めることも大切です。
ユニットテスト導入について上司を説得できません
ユニットテストは、製品を批評するテストではなく、チームを支援するテストです。ユニットテストを実践する事で、リグレッションの不安を取り除き、API設計を洗練させ、期待通りに動くという自信を持つことができます。これにより、開発チームが継続的な開発を進めやすくなります。決して、製品の品質を評価しているわけではありません。
上司に品質向上を約束してユニットテストを導入しても、期待されるような効果はありません。失敗してしまったら、その組織でユニットテストを導入することは困難になるでしょう。特にチームがユニットテストに不慣れな場合、慣れるだけで手がいっぱいとなります。時間的コストが増大しただけとなる可能性は高いでしょう。
ユニットテストを導入する効果としては、教育的効果をアピールしてください。ユニットテストを書くこととでプログラミングスキルは高まります。問題のある設計を見抜くスキルも身に付くでしょう。リファクタリングにより、より高品質なコードに改善するための足がかりにもなります。それらは組織にとって大きな利益となるはずです。教育的コストを払い、ユニットテストを導入する方向で説得してください。
随分前から失敗のままのテストコードが残っています
テストコードを書き捨てにするのではなく、ユニットテストをプロセスに組み込んでいる場合、テストコードはプロダクションコードと共にメンテナンスし続ける必要があります。プロダクションコードになんらかの変更が行われる場合はユニットテストにも影響があり、なんらかの変更が行われるのが自然です。したがって、単純に変更作業の時間を考えた場合、ユニットテストを実践していない方が短い時間で終わる場合もあるでしょう。そして、テストコードのメンテナンス作業は非常に手間のかかる作業です。
ユニットテストは常に成功するように維持し、失敗したならならばなるべく早く成功する状態に戻さなければなりません。傷が浅いうちの方が修正も簡単です。長らく失敗のままで放置されているテストコードがあるならば、再び成功させることは困難です。また、テストコードとしての価値は既になくなっているので、早々に捨てましょう。もし、テストがなくて不安であるならば新規にテストコードを追加してください。
テストコードの修正は早い段階から継続的に行ってください。重複を減らし、メンテナンスしやすい状態を保ちます。
納品物としてテスト仕様書などが求められています
ドキュメントベースの単体テストを置き換えている場合や、JUnitなどのツールを利用すれば良いと思っている場合、ユニットテスト自体の質よりも、テスト件数やカバレッジなど数値や成果物を強く求められる傾向があります。これはプログラマにとっては望ましい状態ではありません。しかし、立場的に数値や成果物がなくては困る人もいることを忘れないでください。
なんらかの形で数値や成果物を必要としている人がいるのであれば、JavaDocなどのドキュメントを生成して印刷したり、カバレッジの測定結果などをまとめて成果物とします。なるべく自動生成できるものとすべきです。無駄な作業かもしれませんが、それでユニットテストを実践できるのであれば、必要なコストと我慢する方が良いと思います。
ただし、カバレッジやテスト件数などの数字に対する合意はしっかりととっておくべきです。特に途中経過を報告しなければならなくなった場合など、テスト件数が足りない/足りているといった根拠のない判断をされれてしまう場合があります。特に数値や成果物を重視する人達にとって、テスト件数やステップ数が減ることは許容できない問題です。なるべく最終結果のみを報告するように調整してください。
それでも説得できない場合や、自動生成ではどうにもならない手間のかかる成果物が必須だと平行線となる場合は、ユニットテストの導入は諦めた方が良いでしょう。余計な手間ばかりが増え、すべてが中途半端になり、誰もシアワセになることはありません。
レガシーコードが相手なのですが、どうしたらよいでしょうか?
あなたが余程のユニットテストの熟練者でもない限り、レガシーコードを相手にユニットテストを実践しようとすることは無謀としか言いようがありません。レガシーコードは、ユニットテスト不可能であることが、レガシーコードたる所以なのです。
レガシーコードにはテストがありません。可読性も低く、重複も多いでしょう。メソッドやクラスの意図が読み取れることもありません。環境依存のコードも多く混在しています。これらはすべてユニットテストを難しくする要因です。
また、レガシーコードに変更を加えると、予期せぬ場所に影響があるリスクもあります。「動いているモノは触らない」というのはテストのないレガシーコードでは仕方ないのです。そして、レガシーコードでは、そもそも正しく動いていない可能性すらあります。レガシーコードを相手にする場合、高いコストを払ってユニットテストしても、大きなメリットにはなりにくいのです。
なお、レガシーコードを相手にしたとき、ユニットテストにチャレンジすることは良い経験となります。しかし、全く歯が立たず諦めることになると思います。それで良いのです。頑張りすぎてはいけません。代わりに、どうしたらテストしやすかったのか?何が原因でテストしにくいのか?などを学んでください。そして、次のプロジェクトに生かすべきです。
リソース(人、時間)が足りません
リソース不足な状態で、ユニットテストを導入しても大きな効果はありません。なぜならば、ユニットテストを実践するには十分なトレーニングが必要不可欠であり、コストも高いからです。もし、ユニットテストを導入したプロジェクトが失敗したならば、ユニットテストを導入したことが原因とされる可能性もあります。そうなってしまったら、その組織ではユニットテストを導入しずらくなるでしょう。チームのメンバーからも「ただでさえ忙しいのに、やること増やしやがって」と思われるだけです。
ユニットテストを導入していくには、ある程度の余裕が必要不可欠です。特にメンバーにスキルが不足している場合や興味がない場合、業務時間を使ったトレーニングが必要となります。そうでなければ、リソースは動作確認や結合テストに割いた方が良いでしょう。
もし、慢性的にリソース不足であるならば、その組織自体に問題があります。
まとめ
ユニットテスト改善していく中で様々な問題にあたります。それれの問題は一筋縄ではいきませんが、大きく技術的な問題と組織的な問題に分類できます。技術的な問題は、トレーニングや段階的な導入で解決していくことができるでしょう。組織的な問題でも内部的な問題であれば、仲間を増やしたり他の手段の代替手段として導入することで改善していくことができます。しかし、外部的な組織の問題である場合、成果物を工夫するなどより多くの工夫と労力が必要となってきます。そして、それらの苦労も根拠のない理由で拒絶されるかもしれません。
ユニットテストのない開発からユニットテストのある開発へ、少しずつ改善していくのはやり甲斐のある仕事です。しかし、純粋にユニットテストが行いたいと思うのであれば、ユニットテストのある開発への扉を開くことです。